Skip to content

fix(a11y): Accessibility improvements for WCAG 2.1 Level A compliance#38342

Closed
Aitema-gmbh wants to merge 44 commits intoapache:masterfrom
Aitema-gmbh:fix/a11y-improvements-v3
Closed

fix(a11y): Accessibility improvements for WCAG 2.1 Level A compliance#38342
Aitema-gmbh wants to merge 44 commits intoapache:masterfrom
Aitema-gmbh:fix/a11y-improvements-v3

Conversation

@Aitema-gmbh
Copy link
Copy Markdown
Contributor

@Aitema-gmbh Aitema-gmbh commented Mar 2, 2026

User description

Summary

This PR introduces accessibility improvements to Apache Superset for WCAG 2.1 Level A compliance. It supersedes #37211 with a clean rebase on current master and addresses all review feedback from the previous PR.

New Accessibility Components

  • SkipLink: Skip-to-content navigation for keyboard users (WCAG 2.4.1 Bypass Blocks)

    • Visually hidden, appears on focus
    • Temporary tabindex management — sets -1 only during focus, removes on blur to preserve natural tab order
    • Fallback to hash navigation if target element not found
  • StatusAnnouncer: ARIA live region for screen reader announcements (WCAG 4.1.2 Name, Role, Value)

    • Context-based provider pattern with useStatusAnnouncer hook
    • Supports polite and assertive politeness levels
    • Auto-clears messages after 5 seconds

Enhanced Semantic Structure

  • SliceHeader: Added semantic <h2> headings for chart titles (WCAG 1.3.1 Info and Relationships)
    • Heading inherits all visual styles — zero visual change
    • Minimal diff: only wrapping element + CSS reset added

Changes from previous PR #37211

  • Rebased on current master (fixes stale import paths)
  • Uses @apache-superset/core/ui and @superset-ui/core/components import structure
  • Fixed SkipLink tabindex bug — now temporary instead of permanent (addresses Bito, CodeAnt, Copilot feedback)
  • No longer modifies ActionButtons — upstream already has correct keyboard/filter logic
  • Removed Storybook stories to keep PR focused (can be added separately)

WCAG Criteria Addressed

Criterion Level Implementation
1.3.1 Info and Relationships A Semantic <h2> headings for chart titles
2.1.1 Keyboard A SkipLink keyboard navigation
2.4.1 Bypass Blocks A Skip-to-content link
4.1.2 Name, Role, Value A ARIA live regions via StatusAnnouncer

Testing Instructions

  1. Skip Link: Press Tab on page load — skip link should appear, pressing Enter jumps to main content
  2. Keyboard Navigation: Tab through dashboard — chart titles are announced as headings by screen readers
  3. Screen Reader: Verify <h2> chart titles are properly announced with heading level

Checklist


CodeAnt-AI Description

Improve accessibility across charts, forms, navigation, and error handling

What Changed

  • Added skip navigation, live status messages, and clearer page headings so keyboard and screen reader users can move through the app and understand page structure
  • Made charts, lists, buttons, menus, popovers, and status icons readable without color alone, with clearer labels, focus states, and Escape-to-close behavior
  • Linked form errors to their fields, added helpful validation text and browser autocomplete hints, and improved responsive layouts for narrow screens and zoomed text
  • Added page-level accessibility tests, regression checks, and setup guides to keep these changes in place

Impact

✅ Faster keyboard navigation
✅ Clearer screen reader announcements
✅ Fewer form-entry and login errors

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Fedo Hagge-Kubat added 3 commits March 2, 2026 14:23
- SkipLink: WCAG 2.4.1 Bypass Blocks - skip-to-content navigation for keyboard users
- StatusAnnouncer: WCAG 4.1.2 Name, Role, Value - ARIA live region for screen readers
- Fixed tabindex handling: temporary tabindex on focus, removed on blur
- Uses current @apache-superset/core/ui import structure
Wraps chart titles in semantic <h2> elements for screen reader accessibility.
The heading inherits all visual styles from the parent container so the
visual appearance remains unchanged.
- SkipLink: tests for rendering, focus management, temporary tabindex,
  existing tabindex preservation, and fallback hash navigation
- StatusAnnouncer: tests for ARIA live regions, polite/assertive messages,
  message timeout clearing, and provider rendering
@dosubot dosubot Bot added change:frontend Requires changing the frontend design:accessibility Related to accessibility standards labels Mar 2, 2026
@Aitema-gmbh
Copy link
Copy Markdown
Contributor Author

Hi @geido @kgabryje @michael-s-molina @rusackas 👋

This is a clean rewrite of #37211, rebased on current master with all bot review feedback addressed:

  • ✅ SkipLink tabindex bug fixed (temporary on focus, removed on blur)
  • ✅ Correct import paths (@apache-superset/core/ui, @superset-ui/core/components)
  • ✅ No more ActionButtons modifications (upstream logic is already correct)
  • ✅ Minimal SliceHeader diff — just a semantic <h2> wrapper + CSS reset

The scope is intentionally focused: 2 new accessibility components (SkipLink, StatusAnnouncer) + 1 small semantic heading change. Unit tests included.

Would really appreciate a review when you have time! Happy to address any feedback. 🙏

Comment on lines +100 to +114
it('prevents default link behavior', () => {
const targetEl = document.createElement('div');
targetEl.id = 'main-content';
document.body.appendChild(targetEl);

render(<SkipLink />);
const link = screen.getByText('Skip to main content');

const preventDefaultSpy = jest.fn();
link.addEventListener('click', preventDefaultSpy, { once: true });

fireEvent.click(link);

document.body.removeChild(targetEl);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The "prevents default link behavior" test sets up a spy but never makes any assertion about it, so the test always passes and does not actually verify that the default navigation is prevented; add a concrete assertion that the location hash remains unchanged when the target exists to ensure the behavior is exercised. [logic error]

Severity Level: Major ⚠️
- ⚠️ SkipLink default-prevention regressions not detected by unit tests.
- ⚠️ Accessibility behavior may break without automated test signal.
Suggested change
it('prevents default link behavior', () => {
const targetEl = document.createElement('div');
targetEl.id = 'main-content';
document.body.appendChild(targetEl);
render(<SkipLink />);
const link = screen.getByText('Skip to main content');
const preventDefaultSpy = jest.fn();
link.addEventListener('click', preventDefaultSpy, { once: true });
fireEvent.click(link);
document.body.removeChild(targetEl);
});
it('prevents default link behavior', () => {
const targetEl = document.createElement('div');
targetEl.id = 'main-content';
document.body.appendChild(targetEl);
// Set an initial hash to detect unintended navigation
window.location.hash = '#initial';
render(<SkipLink />);
const link = screen.getByText('Skip to main content');
fireEvent.click(link);
// Clicking the skip link should not change the location hash when the target exists
expect(window.location.hash).toBe('#initial');
document.body.removeChild(targetEl);
});
Steps of Reproduction ✅
1. Run the unit tests for the accessibility components with `npm test --
SkipLink.test.tsx`, which executes
`superset-frontend/src/components/Accessibility/SkipLink.test.tsx`.

2. Observe the test case `it('prevents default link behavior', () => { ... })` at lines
100–114 in `SkipLink.test.tsx`; it sets up a `preventDefaultSpy` and calls
`fireEvent.click(link)` but contains no `expect(...)` assertions.

3. In `superset-frontend/src/components/Accessibility/SkipLink.tsx` (the component
imported via `import SkipLink from './SkipLink';`), temporarily remove or comment out the
logic that prevents default navigation on click (e.g., the `event.preventDefault()` call
in the click handler).

4. Re-run `npm test -- SkipLink.test.tsx` and note that all tests, including `'prevents
default link behavior'`, still pass despite the regression, because the test at lines
100–114 never checks either the spy or `window.location.hash`, so it cannot fail based on
the default-prevention behavior.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/components/Accessibility/SkipLink.test.tsx
**Line:** 100:114
**Comment:**
	*Logic Error: The "prevents default link behavior" test sets up a spy but never makes any assertion about it, so the test always passes and does not actually verify that the default navigation is prevented; add a concrete assertion that the location hash remains unchanged when the target exists to ensure the behavior is exercised.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

Comment on lines +25 to +90
useMemo,
useRef,
useState,
} from 'react';
import { css } from '@apache-superset/core/ui';

/**
* StatusAnnouncer - WCAG 4.1.2 Name, Role, Value
* Provides an ARIA live region for announcing dynamic content updates
* to assistive technologies (screen readers).
*/

type Politeness = 'polite' | 'assertive';

interface StatusAnnouncerContextType {
announce: (message: string, politeness?: Politeness) => void;
}

const StatusAnnouncerContext = createContext<StatusAnnouncerContextType>({
announce: () => {},
});

export const useStatusAnnouncer = () => useContext(StatusAnnouncerContext);

const visuallyHiddenStyle = css`
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;

interface StatusAnnouncerProviderProps {
children: ReactNode;
}

export const StatusAnnouncerProvider: FC<StatusAnnouncerProviderProps> = ({
children,
}) => {
const [politeMessage, setPoliteMessage] = useState('');
const [assertiveMessage, setAssertiveMessage] = useState('');
const clearTimeoutRef = useRef<ReturnType<typeof setTimeout>>();

const announce = useCallback(
(message: string, politeness: Politeness = 'polite') => {
if (clearTimeoutRef.current) {
clearTimeout(clearTimeoutRef.current);
}

if (politeness === 'assertive') {
setAssertiveMessage(message);
} else {
setPoliteMessage(message);
}

clearTimeoutRef.current = setTimeout(() => {
setPoliteMessage('');
setAssertiveMessage('');
}, 5000);
},
[],
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The timeout that clears the polite/assertive messages is never cleaned up on unmount, so if the provider is unmounted within 5 seconds of an announcement the timeout callback will still fire and attempt to update state on an unmounted component, causing React warnings and a small memory/resource leak; add a cleanup that clears any pending timeout when the provider unmounts. [resource leak]

Severity Level: Major ⚠️
- ⚠️ React warns about state updates after unmount.
- ⚠️ Tests using StatusAnnouncerProvider may become flaky.
- ⚠️ Minor timer leak when unmounting within five seconds.
Suggested change
useMemo,
useRef,
useState,
} from 'react';
import { css } from '@apache-superset/core/ui';
/**
* StatusAnnouncer - WCAG 4.1.2 Name, Role, Value
* Provides an ARIA live region for announcing dynamic content updates
* to assistive technologies (screen readers).
*/
type Politeness = 'polite' | 'assertive';
interface StatusAnnouncerContextType {
announce: (message: string, politeness?: Politeness) => void;
}
const StatusAnnouncerContext = createContext<StatusAnnouncerContextType>({
announce: () => {},
});
export const useStatusAnnouncer = () => useContext(StatusAnnouncerContext);
const visuallyHiddenStyle = css`
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
interface StatusAnnouncerProviderProps {
children: ReactNode;
}
export const StatusAnnouncerProvider: FC<StatusAnnouncerProviderProps> = ({
children,
}) => {
const [politeMessage, setPoliteMessage] = useState('');
const [assertiveMessage, setAssertiveMessage] = useState('');
const clearTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const announce = useCallback(
(message: string, politeness: Politeness = 'polite') => {
if (clearTimeoutRef.current) {
clearTimeout(clearTimeoutRef.current);
}
if (politeness === 'assertive') {
setAssertiveMessage(message);
} else {
setPoliteMessage(message);
}
clearTimeoutRef.current = setTimeout(() => {
setPoliteMessage('');
setAssertiveMessage('');
}, 5000);
},
[],
);
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { css } from '@apache-superset/core/ui';
/**
* StatusAnnouncer - WCAG 4.1.2 Name, Role, Value
* Provides an ARIA live region for announcing dynamic content updates
* to assistive technologies (screen readers).
*/
type Politeness = 'polite' | 'assertive';
interface StatusAnnouncerContextType {
announce: (message: string, politeness?: Politeness) => void;
}
const StatusAnnouncerContext = createContext<StatusAnnouncerContextType>({
announce: () => {},
});
export const useStatusAnnouncer = () => useContext(StatusAnnouncerContext);
const visuallyHiddenStyle = css`
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
interface StatusAnnouncerProviderProps {
children: ReactNode;
}
export const StatusAnnouncerProvider: FC<StatusAnnouncerProviderProps> = ({
children,
}) => {
const [politeMessage, setPoliteMessage] = useState('');
const [assertiveMessage, setAssertiveMessage] = useState('');
const clearTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const announce = useCallback(
(message: string, politeness: Politeness = 'polite') => {
if (clearTimeoutRef.current) {
clearTimeout(clearTimeoutRef.current);
}
if (politeness === 'assertive') {
setAssertiveMessage(message);
} else {
setPoliteMessage(message);
}
clearTimeoutRef.current = setTimeout(() => {
setPoliteMessage('');
setAssertiveMessage('');
}, 5000);
},
[],
);
useEffect(() => {
return () => {
if (clearTimeoutRef.current) {
clearTimeout(clearTimeoutRef.current);
}
};
}, []);
Steps of Reproduction ✅
1. Render `StatusAnnouncerProvider` (default export from
`superset-frontend/src/components/Accessibility/StatusAnnouncer.tsx:65`) in a React tree
with any child component.

2. Inside a child component, call `const { announce } = useStatusAnnouncer();
announce('Loading...', 'polite');` which executes the callback in `announce()` at
`StatusAnnouncer.tsx:72-88`, setting `clearTimeoutRef.current = setTimeout(...)`.

3. Unmount the subtree containing `StatusAnnouncerProvider` within 5 seconds (for example,
in a route change or in a React Testing Library test calling `unmount()` on the rendered
tree).

4. After unmount, the pending timeout fires and runs the callback at
`StatusAnnouncer.tsx:84-86`, calling `setPoliteMessage('')` / `setAssertiveMessage('')` on
an unmounted component, which in React produces "Can't perform a React state update on an
unmounted component" warnings and keeps the timer alive until it finishes.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/components/Accessibility/StatusAnnouncer.tsx
**Line:** 25:90
**Comment:**
	*Resource Leak: The timeout that clears the polite/assertive messages is never cleaned up on unmount, so if the provider is unmounted within 5 seconds of an announcement the timeout callback will still fire and attempt to update state on an unmounted component, causing React warnings and a small memory/resource leak; add a cleanup that clears any pending timeout when the provider unmounts.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

Copy link
Copy Markdown
Contributor

@bito-code-review bito-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #e1a659

Actionable Suggestions - 1
  • superset-frontend/src/components/Accessibility/SkipLink.tsx - 1
    • Missing translation for user-facing text · Line 63-63
Additional Suggestions - 1
  • superset-frontend/src/components/Accessibility/SkipLink.test.tsx - 1
    • Incomplete Test Assertion · Line 100-114
      The test 'prevents default link behavior' sets up a spy on the click event but lacks an assertion, so it doesn't verify the claimed behavior. Per BITO.md [6262], tests must assert actual logic beyond mere rendering. This could allow silent failures in event handling.
      Code suggestion
       @@ -112,2 +112,3 @@
      -    fireEvent.click(link);
      -
      +    fireEvent.click(link);
      +    expect(preventDefaultSpy).toHaveBeenCalled();
      +
Review Details
  • Files reviewed - 6 · Commit Range: da4fbd7..e5a2129
    • superset-frontend/src/components/Accessibility/SkipLink.test.tsx
    • superset-frontend/src/components/Accessibility/SkipLink.tsx
    • superset-frontend/src/components/Accessibility/StatusAnnouncer.test.tsx
    • superset-frontend/src/components/Accessibility/StatusAnnouncer.tsx
    • superset-frontend/src/components/Accessibility/index.tsx
    • superset-frontend/src/dashboard/components/SliceHeader/index.tsx
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo


const SkipLink: FC<SkipLinkProps> = ({
targetId = 'main-content',
children = 'Skip to main content',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing translation for user-facing text

The default children prop 'Skip to main content' is hardcoded and should be wrapped with the translation function t() to support internationalization, as per the project's i18n standards.

Code suggestion
Check the AI-generated fix before applying
  import { FC, MouseEvent } from 'react';
 +import { t } from '@apache-superset/core';
  import { styled } from '@apache-superset/core/ui';
 @@ -63,1 +64,1 @@
 -  children = 'Skip to main content',
 +  children = t('Skip to main content'),

Code Review Run #e1a659


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Fedo Hagge-Kubat and others added 9 commits March 2, 2026 15:27
- Add role="img" and aria-label to Chart container with chart name and viz type
- Add visually-hidden screen-reader text to AlertStatusIcon with status label
- Add visually-hidden screen-reader text to TaskStatusIcon with status label
- Mark decorative icon SVGs as aria-hidden="true"
- Add unit tests for Chart container accessibility attributes
- Add unit tests for AlertStatusIcon screen-reader text across all states

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…(WCAG 1.1.1)

- Add aria-label to FullscreenExitOutlined button in SliceHeaderControls
- Add aria-label to Undo/Redo buttons in Dashboard Header (matching tooltip text)
- Mark decorative icons as aria-hidden in SliceHeaderControls download menu
- Mark decorative icons as aria-hidden in Header (Undo, Redo, Save icons)
- Update BaseIconComponent to respect aria-hidden prop (role=presentation, no aria-label)
- Mark decorative icons in Explore header, SQL Lab buttons (Save, Share, Download, Copy)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add aria-pressed to auto-refresh pause/resume toggle button
- Add aria-pressed to edit dashboard mode toggle button
- Add aria-label to DrillBy display mode radio group
- Add aria-expanded and aria-label to filter bar collapse/expand buttons
- Add tabIndex to collapsed filter bar for keyboard accessibility
- Add tests for aria-pressed on edit and auto-refresh toggle buttons
- Add tests for aria-expanded on filter bar collapse/expand controls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…essages (WCAG 1.4.1)

- Replace generic icons in AlertStatusIcon with distinct shapes per status:
  Success=CheckCircleOutlined, Error=CloseCircleOutlined, Working=LoadingOutlined,
  Noop=ClockCircleOutlined, Grace=ExclamationCircleOutlined
- Add "Error:" text prefix to ChartErrorMessage and ImportModal ErrorAlert
- Add aria-label with scheme name and role="option" to ColorSchemeLabel swatches
- Mark individual color swatches as aria-hidden for cleaner screen reader output
- Add tests verifying distinct icon rendering per status, icon presence in ErrorAlert,
  and aria-label on ColorSchemeLabel
- Add aria-atomic="true" to base Alert component for complete message announcement
- Add aria-atomic="true" to BasicErrorAlert custom div container
- Add WCAG 3.3.1 accessibility tests to Alert, BasicErrorAlert, and ErrorAlert
- Verify role="alert", aria-atomic, aria-live attributes across error components
- Test assertive aria-live for errors, polite for warnings/info/success

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… 3.3.1)

- ModalFormField: auto-inject aria-invalid/aria-describedby on child inputs when error prop is set
- LabeledErrorBoundInput: add aria-invalid and aria-describedby linking to error messages
- SqlAlchemyForm: add on-blur validation with aria-invalid for Display Name and URI fields
- SSHTunnelForm: add on-blur validation with aria-invalid for SSH Host, Port, Username fields
- AlertReportModal: add field-level error messages for name and owners via ModalFormField
- NumberInput: extend interface to accept aria-invalid and aria-describedby props
- TextControl: add aria-invalid/aria-describedby and visible error span when validationErrors present
- NumberControl: add aria-invalid/aria-describedby and visible error span when validationErrors present
- ControlHeader: add sr-only error span with ID for aria-describedby linkage
- RoleFormItems: add aria-required to role name input

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 2, 2026

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit f54a896
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/69fca2f215667c0007173ec3
😎 Deploy Preview https://deploy-preview-38342--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Fedo Hagge-Kubat and others added 5 commits March 2, 2026 17:46
…erer

- Replace CanvasRenderer with SVGRenderer in Echart.tsx for all 13+ chart types
- All chart text (labels, axes, legends) now renders as SVG DOM elements, not canvas bitmaps
- Text is selectable, scalable, and accessible to assistive technology
- Add role="img", aria-label, and <title> to chart-card-fallback.svg
- Tooltips remain unaffected (already HTML DOM elements via appendToBody)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…xt resize

- Replace hardcoded 12px font-sizes with inherit/theme tokens in error messages
- Convert fixed height:24px to min-height:24px in SliceHeader controls
- Replace px-based line-heights with unitless values (1, 1.2, 1.5)
- Remove fixed max-width on brand text, add flex-wrap for nav wrapping at zoom
- Replace fontSize:22 with theme.fontSizeXL in fullscreen icon
- Replace font-size:14px with theme.fontSize in ScheduleQueryButton
- Replace fontSize:'12px' with theme.fontSizeSM in SparklineCell tooltip
- Replace font-size:12px with theme.fontSizeSM in CustomizationsBadge icon

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add media queries to dashboard components for narrow viewports
- Stack filter sidebar above content at mobile breakpoints
- Make modal dialogs full-width on small screens
- Prevent horizontal overflow in navigation header

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace colorSplit (~1.04:1) with colorTextTertiary (~3.54:1) for ECharts axis/grid lines
- Add global CSS overrides for Ant Design input borders (colorTextTertiary, 3.54:1)
- Strengthen focus indicators: colorPrimaryBorderHover → colorPrimary (~4.68:1)
- Fix controlOutline in TimeFilterPlugin focus shadow (was low-alpha, now colorPrimary)
- Replace colorPrimaryBorder with colorPrimary in filter focus highlight styles
- Override Ant Design divider border color to colorTextTertiary
- Replace colorSplit in RangeFilterPlugin divider with colorTextTertiary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add responsive wrapping to SubMenu at 480px breakpoint
- Add full-width filter controls at narrow viewports (GroupByFilterCard)
- Add responsive max-width override for FilterDivider h3 at 480px

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bito-code-review
Copy link
Copy Markdown
Contributor

bito-code-review Bot commented Mar 2, 2026

Code Review Agent Run #cf51e6

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset-frontend/src/explore/components/controls/NumberControl/index.tsx - 1
    • Duplicate error display · Line 74-78
      It looks like validationErrors are passed to ControlHeader, which already displays errors via tooltip, and now also shown inline, causing duplication. Omit from ControlHeader props.
      Code suggestion
       @@ -74,32 +74,32 @@
      -  const hasErrors =
      -    rest.validationErrors && rest.validationErrors.length > 0;
      -  const errorId = hasErrors
      -    ? `${rest.name || 'number-control'}-error`
      -    : undefined;
      -
      -  return (
      -    <FullWidthDiv>
      -      <ControlHeader {...rest} />
      -      <FullWidthInputNumber
      -        min={min}
      -        max={max}
      -        step={step}
      -        placeholder={placeholder}
      -        value={value}
      -        onChange={handleChange}
      -        onBlur={handleBlur}
      -        disabled={disabled}
      -        aria-label={rest.label}
      -        aria-invalid={hasErrors || undefined}
      -        aria-describedby={errorId}
      -        id={rest.name}
      -      />
      -      {hasErrors && (
      -        <span
      -          id={errorId}
      -          role="alert"
      -          style={{ color: 'red', fontSize: '12px', display: 'block', marginTop: '4px' }}
      -        >
      -          {rest.validationErrors!.join('. ')}
      -        </span>
      -      )}
      +  const { validationErrors, ...headerProps } = rest;
      +  const hasErrors = validationErrors && validationErrors.length > 0;
      +  const errorId = hasErrors
      +    ? `${headerProps.name || 'number-control'}-error`
      +    : undefined;
      +
      +  return (
      +    <FullWidthDiv>
      +      <ControlHeader {...headerProps} />
      +      <FullWidthInputNumber
      +        min={min}
      -        max={max}
      -        step={step}
      -        placeholder={placeholder}
      -        value={value}
      -        onChange={handleChange}
      -        onBlur={handleBlur}
      -        disabled={disabled}
      -        aria-label={rest.label}
      -        aria-invalid={hasErrors || undefined}
      -        aria-describedby={errorId}
      -        id={rest.name}
      -      />
      -      {hasErrors && (
      -        <span
      -          id={errorId}
      -          role="alert"
      -          style={{ color: 'red', fontSize: '12px', display: 'block', marginTop: '4px' }}
      -        >
      -          {validationErrors!.join('. ')}
      -        </span>
      -      )}
      +        max={max}
      +        step={step}
      +        placeholder={placeholder}
      +        value={value}
      +        onChange={handleChange}
      +        onBlur={handleBlur}
      +        disabled={disabled}
      +        aria-label={headerProps.label}
      +        aria-invalid={hasErrors || undefined}
      +        aria-describedby={errorId}
      +        id={headerProps.name}
      +      />
      +      {hasErrors && (
      +        <span
      +          id={errorId}
      +          role="alert"
      +          style={{ color: 'red', fontSize: '12px', display: 'block', marginTop: '4px' }}
      +        >
      +          {validationErrors!.join('. ')}
      +        </span>
      +      )}
  • superset-frontend/src/features/databases/DatabaseModal/SSHTunnelForm.tsx - 1
    • Inline styles on error spans · Line 98-98
      Inline styles on error spans (lines 98, 128, 159) violate the guideline to avoid custom CSS. Use a styled component with theme tokens instead for consistency with antd best practices.
Review Details
  • Files reviewed - 40 · Commit Range: e5a2129..4141f03
    • superset-frontend/src/components/Chart/Chart.test.tsx
    • superset-frontend/src/components/Chart/Chart.tsx
    • superset-frontend/src/features/alerts/components/AlertStatusIcon.test.tsx
    • superset-frontend/src/features/alerts/components/AlertStatusIcon.tsx
    • superset-frontend/src/features/tasks/TaskStatusIcon.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Icons/BaseIcon.tsx
    • superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
    • superset-frontend/src/SqlLab/components/ResultSet/index.tsx
    • superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
    • superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
    • superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx
    • superset-frontend/src/dashboard/components/Header/index.tsx
    • superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
    • superset-frontend/src/explore/components/ExploreChartHeader/index.tsx
    • superset-frontend/src/components/Chart/DrillBy/useDisplayModeToggle.tsx
    • superset-frontend/src/dashboard/components/AutoRefreshIndicator/index.tsx
    • superset-frontend/src/dashboard/components/Header/Header.test.tsx
    • superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
    • superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/Header.test.tsx
    • superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx
    • superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx
    • superset-frontend/src/components/Chart/ChartErrorMessage.tsx
    • superset-frontend/src/components/ErrorMessage/ErrorAlert.test.tsx
    • superset-frontend/src/components/ImportModal/ErrorAlert.tsx
    • superset-frontend/src/explore/components/controls/ColorSchemeControl/ColorSchemeLabel.test.tsx
    • superset-frontend/src/explore/components/controls/ColorSchemeControl/ColorSchemeLabel.tsx
    • superset-frontend/packages/superset-core/src/ui/components/Alert/Alert.test.tsx
    • superset-frontend/packages/superset-core/src/ui/components/Alert/index.tsx
    • superset-frontend/src/components/ErrorMessage/BasicErrorAlert.test.tsx
    • superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.tsx
    • superset-frontend/src/components/Modal/ModalFormField.tsx
    • superset-frontend/src/explore/components/ControlHeader.tsx
    • superset-frontend/src/explore/components/controls/NumberControl/index.tsx
    • superset-frontend/src/explore/components/controls/TextControl/index.tsx
    • superset-frontend/src/features/alerts/AlertReportModal.tsx
    • superset-frontend/src/features/alerts/components/NumberInput.tsx
    • superset-frontend/src/features/databases/DatabaseModal/SSHTunnelForm.tsx
    • superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx
    • superset-frontend/src/features/roles/RoleFormItems.tsx
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@Aitema-gmbh
Copy link
Copy Markdown
Contributor Author

Individual WCAG PRs Created

This combined PR has been split into 16 individual PRs for easier review — one per WCAG 2.1 criterion:

Level A

Level AA

Each PR is independent and can be reviewed/merged separately. This PR remains open as a reference for the complete scope of accessibility work.

Funded by IMTB.

@rusackas
Copy link
Copy Markdown
Member

Thanks for the huge amount of work here. Given that this has been split into 16 per-WCAG-criterion PRs (#39230#39245) and the branch is now conflicting with master, I'd suggest closing this one and letting the individual PRs carry the review. Happy to continue reviewing the individual PRs once we confirm this can be closed.

@rusackas
Copy link
Copy Markdown
Member

@Aitema-gmbh thanks for picking this back up after #37211. Before I can take another deeper pass, a few things need attention:

  • The branch has merge conflicts against master (mergeStateStatus is DIRTY) — needs a rebase.
  • There are ~30 unresolved bot review threads from codeant, bito, and copilot, and a fair number of them look like real issues rather than noise. Some highlights:
    • Test files that won't run as written: Home.a11y.test.tsx (fetchMock.clearHistory() — not a method), Chart.a11y.test.tsx (fetchMock.callHistory.calls — not a property), a11y-regression.test.tsx (uses Node fs under jsdom).
    • Production regressions: Popover/index.tsx drops the legacy visible/onVisibleChange API still used by callers like TaskPayloadPopover; Timeseries/transformers.ts wcagSymbol block re-enables symbols on confidence-band series; GlobalStyles progress min-width makes 0% progress render as started; ModalFormField.tsx uses React.ReactElement without importing React.
    • Accessibility regressions in an a11y PR: 404.html/500.html CTA text set to ~11px; Chart.tsx role="img" wrapping interactive content; views/App.tsx truncates pt_BRpt.
    • Repeated pattern of role="button" + tabIndex={0} without onKeyDown (ChartList edit/export/delete, TaskList cancel, Dropdown MenuDotsWrapper, CollapsedBar) — keyboard users still can't activate them.

Also: at 154 files / +6.2k lines this PR is genuinely too large to review well in one go. Could you split it into focused PRs — at minimum separating (1) the new SkipLink/StatusAnnouncer components, (2) the global style + 404/500 changes, (3) the ECharts/plugin changes, (4) the list-page row-action keyboard fixes, and (5) the new docs? Smaller PRs will move much faster.

Let me know once those are addressed and I'll take another look.

@koljaschumann
Copy link
Copy Markdown

Thanks @rusackas — agreed. Closing this in favor of the focused per-WCAG PRs (#39230#39245). Two of those (#39243, #39244) have already merged, and @Aitema-gmbh is actively addressing the bot/review feedback on the rest.

Appreciate the time you've put into reviewing this work.

Fedo Hagge-Kubat and others added 16 commits May 7, 2026 16:33
…guide (QA-04)

- WCAG Compliance Matrix mapping all 15 criteria to files, commits, and tests
- Accessibility Release Notes documenting all changes by sprint
- Developer Guide with 9 reusable accessibility patterns and code examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…UG-07)

- BUG-01 (2.4.6): Add sr-only h1 headings to DashboardList, ChartList, SqlLab
- BUG-02 (3.3.3): Login error with correction suggestion, aria-describedby, aria-invalid
- BUG-03 (1.4.11/2.4.7): Override Ant Design focus ring with !important for 3:1 contrast
- BUG-04 (1.4.3): Change link color from #2893B3 (3.55:1) to #0d7090 (5.62:1)
- BUG-05 (2.4.1): Integrate SkipLink into App.tsx with main-content target
- BUG-06 (4.1.3): Integrate StatusAnnouncerProvider into App.tsx
- BUG-07 (4.1.2): Add aria-label to icon-only buttons (More, Import)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add html { font-size: 100% } in GlobalStyles.tsx to ensure base font
  size respects browser preferences (WCAG 1.4.4)
- Replace React 18 useId() with useState() in Login/index.tsx and
  Modal/ModalFormField.tsx for React 17 compatibility
- Fixes login page crash: "TypeError: useId is not a function"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The error message now avoids assuming credential failure, while still
providing a correction suggestion as required by WCAG 3.3.3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1.4.4: Convert 12 font-size px declarations to rem across SubMenu,
UserInfo, Modal, DatabaseModal, static error pages, CountryMap

1.4.11: Override Ant Design checkbox/radio/switch borders to
colorTextTertiary (~3.54:1), enforce ECharts line-width >= 2px,
fix table borders and progress bar visibility

1.4.13: Add ESC-dismiss and controlled state to Tooltip and Popover
wrappers, make ECharts tooltips hoverable (enterable: true) with
ESC keydown handler on chart container

Also fix: StatusAnnouncer WCAG comment (4.1.2 → 4.1.3),
remove dead link color rule in GlobalStyles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add autoComplete="off" to OAuth2ClientField secret input
- Replace non-standard dynamic autoComplete values with "off" in ImportModal
  (password, ssh_tunnel_password, ssh_tunnel_private_key, ssh_tunnel_private_key_password)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Darken 4 categorical palette colors that had insufficient contrast
against white backgrounds (WCAG 1.4.11 non-text contrast):

- #5AC189 -> #4DA575 (green, 2.23:1 -> 3.02:1)
- #FF7F44 -> #E8733D (orange, 2.51:1 -> 3.02:1)
- #FCC700 -> #B58F00 (yellow, 1.58:1 -> 3.05:1)
- #3CCCCB -> #30A3A2 (cyan, 1.96:1 -> 3.05:1)

Also update sequential/diverging palettes (superset_seq_2, superset_div_2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SqlEditorLeftBar: add @media (max-width: 768px) to override min-width: 500px
- SaveDatasetModal: add @media (max-width: 480px) for form field max-width
- Vertical FilterBar: add @media (max-width: 768px) display: none as safety net

All fixed-width components now reflow correctly at 320px CSS width.
Data tables exempt per WCAG (two-dimensional content).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Datasets was the only main page missing a screen-reader-only h1.
Matches the pattern used on Dashboards, Charts and SQL Lab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
One link on the Home page inherited colorPrimary (3.55:1) via
higher-specificity styled-component rules, bypassing the global
a { color: #0d7090 } rule (5.62:1). Add !important to ensure
all links meet the 4.5:1 contrast threshold.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous !important override applied to all <a> elements, which could
break white-on-dark button links. Narrow selector to exclude ant-btn and
role="button" elements. Verified on live deployment (v13) that primary
buttons, filled buttons, and link-style buttons retain correct colors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds accessible names to 30+ icon-only interactive elements that were
previously announced as just "button" by screen readers:

- SubMenu: pass aria-label prop to import buttons (Dashboard, Chart,
  SavedQuery, Theme)
- ListView: aria-label on grid/list view toggle buttons
- Search filter: labeled clear icon via allowClear config
- FaveStar: dynamic "Add to/Remove from favorites" label
- Action buttons (Edit, Export, Delete, Duplicate, Cancel, Sync) on
  all list pages: Dashboard, Chart, Dataset, Database, Tags, RLS, Task
- Explore: collapse/open datasource panel buttons
- Dashboard: IconButton aria-label fallback, Tabs close icon

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1.4.5: Explicitly set renderer:'svg' in ECharts init() to guarantee SVG
output. The full 'echarts' import in ZoomConfigsChart registered
CanvasRenderer as a side effect, overriding the modular SVGRenderer.
Migrated ZoomConfigsChart and zoomUtil to echarts/core modular imports.

1.4.1: Add distinct dash patterns and symbols per line series so
information is not conveyed by color alone. Applies to both Timeseries
and MixedTimeseries chart types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MR-01: Add aria-labels to 3 icon-only buttons in dashboard edit mode
  (DeleteComponentButton, Column settings, Row settings)
MR-02: Fix secondary button contrast — use colorPrimaryText instead of
  colorPrimaryTextHover for WCAG 1.4.11 compliance (>= 3:1)
MR-03: Remove overflow-x:hidden from header at narrow viewports to allow
  proper menu item reflow at 400% zoom (WCAG 1.4.10)
MR-04: Remove tabIndex={-1} from brand link to restore keyboard navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
colorPrimaryText has sufficient contrast in light mode but fails in
dark mode (~2.7:1 on colorPrimaryBg). Use colorText instead, which
provides >= 4:1 contrast in both light and dark mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ant Design menubar sets tabIndex=-1 on all items except the first,
requiring arrow-key navigation. This useEffect override ensures every
top-level menu item (including submenus like "SQL Lab") gets tabIndex=0,
making them focusable via Tab key as users expect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aitema-gmbh Aitema-gmbh force-pushed the fix/a11y-improvements-v3 branch from a7c2b25 to f54a896 Compare May 7, 2026 14:34
@Aitema-gmbh
Copy link
Copy Markdown
Contributor Author

Thanks @rusackas — agreed. Closing this in favor of the focused per-WCAG PRs (#39230#39245). Two of those (#39243, #39244) have already merged, and @Aitema-gmbh is actively addressing the bot/review feedback on the rest.

Appreciate the time you've put into reviewing this work.

@Aitema-gmbh Aitema-gmbh closed this May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:frontend Requires changing the frontend design:accessibility Related to accessibility standards packages plugins size/XXL size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants